package com.googlecode.cswish.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.CssStyle;
import com.googlecode.cswish.struts.spring.CacheService;
import com.phloc.commons.charset.CCharset;
import com.phloc.css.ECSSVersion;
import com.phloc.css.decl.CSSDeclaration;
import com.phloc.css.decl.CSSExpression;
import com.phloc.css.decl.CSSExpressionMemberTermURI;
import com.phloc.css.decl.CSSMediaRule;
import com.phloc.css.decl.CSSSelector;
import com.phloc.css.decl.CSSSelectorMemberNot;
import com.phloc.css.decl.CSSSelectorSimpleMember;
import com.phloc.css.decl.CSSStyleRule;
import com.phloc.css.decl.CascadingStyleSheet;
import com.phloc.css.decl.ECSSSelectorCombinator;
import com.phloc.css.decl.ICSSExpressionMember;
import com.phloc.css.decl.ICSSSelectorMember;
import com.phloc.css.decl.ICSSTopLevelRule;
import com.phloc.css.reader.CSSReader;
import com.phloc.css.reader.errorhandler.CollectingCSSParseErrorHandler;
import com.phloc.css.reader.errorhandler.DoNothingCSSParseErrorHandler;
import com.phloc.css.reader.errorhandler.LoggingCSSParseErrorHandler;
import com.phloc.css.writer.CSSWriterSettings;

@Service
public class CssManager {

	private static final Log logger = LogFactory.getLog(CssManager.class);

	private CSSWriterSettings cssSettings = new CSSWriterSettings(ECSSVersion.CSS30, true);
	
	@Resource
	private Config config;

	@Resource
	private CacheService cacheService;
	
	// cssName -- cssName
	private Map<String, List<String>> cssInfos;

	public CssManager() {
	}
	
	@PostConstruct
	private void init() {
		cssInfos = cacheService.newCaches(String.class, 20);
	}

	public List<String> getCssInfos(String fileName) {
		List<String> infos = cssInfos.get(fileName);
		if (infos == null) {
			initCache(fileName);
			
			infos = cssInfos.get(fileName);
		}
		return infos;
	}

	private void initCache(String taglibFile) {
		String webRootPath = config.getRealSitePath();

		InputStream input = null;
		try {
			File file = new File(webRootPath + taglibFile);
			String content;
			if (file.exists()) {
				content = FileUtils.readFileToString(file, "UTF-8");
			} else {
				if (taglibFile.startsWith("/static/")) {
					input = this.getClass().getResourceAsStream(taglibFile);
				} else {
					input = this.getClass().getResourceAsStream("/static" + taglibFile);
				}
				if (input != null) {
					content = IOUtils.toString(input);
				} else {
					content = "";
					logger.warn("Failed to read tag file:" + taglibFile);
				}
			}
			parseCssNames(taglibFile, content);
		} catch (Exception ex) {
			logger.error("Failed to read tag file:" + taglibFile, ex);
		} finally {
			IOUtils.closeQuietly(input);
		}
	}

	/*
	protected void initCssFile(String content) {
		// Only scan the simple standard class
		String nameExpression = "([\\w-]+)";
		String expression = "\\." + nameExpression + "\\s*\\{";

		Pattern pattern = Pattern.compile(expression);
		Matcher matcher = pattern.matcher(content);

		while (matcher.find()) {
			String cssName = matcher.group(1);

			cssInfos.put(cssName, cssName);
		}
	}*/

	public List<String> searchCss(String cssContent, String cssName) {
		// Only scan the simple standard class
		String nameExpression = "(" + cssName + "[\\w-]+)";
		String expression = "\\." + nameExpression + "\\s*\\{";

		Pattern pattern = Pattern.compile(expression);
		Matcher matcher = pattern.matcher(cssContent);

		List<String> css = new ArrayList<String>();
		while (matcher.find()) {
			String fullCssName = matcher.group(1);
			css.add(fullCssName);
		}
		return css;
	}

	// TODO: add the default value
	public List<CssStyle> getCssStyles() {
		// styling
		List<CssStyle> styles = new ArrayList<CssStyle>();
		{
			CssStyle cssStyle = new CssStyle();
			styles.add(cssStyle);
			cssStyle.setLabel("Styling");

			cssStyle.add("background-color", "");
			cssStyle.add("background-image", ""); // url('gradient2.png')
			cssStyle.add("background-repeat", ""); // no-repeat, repeat-x,
													// repeat-y
			cssStyle.add("background-attachment", ""); // scroll, fixed, inherit
			cssStyle.add("background-position", ""); // right top

			cssStyle.add("color", ""); // text color, #ffffff
			cssStyle.add("text-align", ""); // center,right,justify
			cssStyle.add("text-decoration", ""); // none,overline,line-through,underline
			cssStyle.add("text-transform", ""); // uppercase,lowercase,capitalize
			cssStyle.add("text-indent", ""); // px

			cssStyle.add("font-family", "");
			cssStyle.add("font-style", ""); // normal,italic,oblique
			cssStyle.add("font-size", ""); // px
			cssStyle.add("font-weight", ""); // normal, bold, 900
		}

		{
			CssStyle cssStyle = new CssStyle();
			styles.add(cssStyle);
			cssStyle.setLabel("Box");

			// content, padding, border, margin
			cssStyle.add("width", ""); // content width
			cssStyle.add("min-width", "");
			cssStyle.add("max-width", "");
			cssStyle.add("height", ""); // content height
			cssStyle.add("min-height", "");
			cssStyle.add("max-height", "");
			cssStyle.add("padding", "");
			cssStyle.add("border", "");
			cssStyle.add("margin", ""); // number, auto

			cssStyle.add("border", "");
			cssStyle.add("border-width", "");
			cssStyle.add("border-style", ""); // none,hidden,dotted,dashed,solid,double,groove,ridge,inset,outset,inherit
			cssStyle.add("border-color", ""); //

			cssStyle.add("outline", "");
			cssStyle.add("outline-width", "");
			cssStyle.add("outline-style", ""); // none,hidden,dotted,dashed,solid,double,groove,ridge,inset,outset,inherit
			cssStyle.add("outline-color", ""); //
		}

		{
			CssStyle cssStyle = new CssStyle();
			styles.add(cssStyle);
			cssStyle.setLabel("Advanced");

			cssStyle.add("display", ""); // inline,block,inline-block,none
			cssStyle.add("visibility", ""); // hidden
			cssStyle.add("position", ""); // static,relative,absolute,fixed
			cssStyle.add("left", "");
			cssStyle.add("top", "");
			cssStyle.add("right", "");
			cssStyle.add("bottom", "");
			cssStyle.add("z-index", "");
			cssStyle.add("overflow", ""); // auto,hidden,scroll,visible,inherit
			cssStyle.add("cursor", ""); // url,auto,crosshair,default,pointer,move,e-resize,ne-resize,nw-resize,n-resize,se-resize,sw-resize
			// s-resize,w-resize,text,wait,help
			cssStyle.add("rect", ""); // left,top,width,height

			cssStyle.add("float", ""); // none,left,right,inherit
			cssStyle.add("clear", ""); // none,left,right,both,inherit

			cssStyle.add("opacity", ""); // number (less 1)
		}
		return styles;
	}

	public static void main(String[] args) throws IOException {
		CssManager cssManager = new CssManager();
		String file = "D:\\workspace\\espirit\\espirit-core\\src\\main\\resources\\static\\styles\\jtaglib.css";
		String content = FileUtils.readFileToString(new File(file));
		cssManager.cssInfos = new HashMap<>();
		cssManager.parseCssNames(file, content);
		
		for (Entry<String, List<String>> entry : cssManager.cssInfos.entrySet()) {
			System.out.println(entry.getValue());
		}
	}

	public void parseCssNames(String fileName, String content) {
		CSSReader.setDefaultParseErrorHandler(DoNothingCSSParseErrorHandler.getInstance());
		CascadingStyleSheet aCSS = CSSReader.readFromString (content, CCharset.CHARSET_UTF_8_OBJ, ECSSVersion.CSS30);
		
		List<String> cssNames = new ArrayList<>();
		this.cssInfos.put(fileName, cssNames);
		
		CSSWriterSettings settings = new CSSWriterSettings(ECSSVersion.CSS30, false);
		for (ICSSTopLevelRule cssTopLevelRule : aCSS.getAllRules()) {
			if (cssTopLevelRule instanceof CSSStyleRule) {
				CSSStyleRule cssStyleRule = (CSSStyleRule) cssTopLevelRule;
				String ruleName = cssStyleRule.getSelectorsAsCSSString(settings, 0);
				if (ruleName.startsWith(".") && ruleName.indexOf(' ') == -1) {
					String formatName = ruleName.substring(1);		// skip '.'
					cssNames.add(formatName);
				}
			}
		}
	}
	
	public String appendPrefix(String content, String prefix, String absoluteUrlPrefix, boolean replaceHtmlBody) {
		try {
			char firstChar = prefix.charAt(0);
			CSSSelectorSimpleMember prefixMember = new CSSSelectorSimpleMember(prefix);
			CSSSelectorMemberNot notMember;
			// TODO: check the browser if it supports the css3
			boolean supportCss3 = true;
			if (supportCss3) {
				if (firstChar == '.' || firstChar == '#') {
					// provide the ability to exclude css affect
					// see core.js, addCssNotFlag
					notMember = new CSSSelectorMemberNot();
					String notSuffix = "" + firstChar + '_' + prefix.substring(1);
					ICSSSelectorMember selector = new CSSSelectorSimpleMember(notSuffix);
					notMember.addSelector(selector);
				} else {
					notMember = null;
				}
			} else {
				notMember = null;
			}
			
			StringBuilder newContent = new StringBuilder();
			try {
				CollectingCSSParseErrorHandler errorHandlers = new CollectingCSSParseErrorHandler(new LoggingCSSParseErrorHandler ());
				CSSReader.setDefaultParseErrorHandler(errorHandlers);
				CascadingStyleSheet css = CSSReader.readFromString (content, CCharset.CHARSET_UTF_8_OBJ, ECSSVersion.CSS30);
				
				for (ICSSTopLevelRule cssTopLevelRule : css.getAllRules()) {
					if (cssTopLevelRule instanceof CSSStyleRule) {
						appendCssStylePrefix((CSSStyleRule)cssTopLevelRule, prefixMember, notMember, absoluteUrlPrefix, replaceHtmlBody);
					} else if (cssTopLevelRule instanceof CSSMediaRule) {
						CSSMediaRule cssMediaRule = (CSSMediaRule) cssTopLevelRule;
						for (ICSSTopLevelRule mediaTopLevelRule : cssMediaRule.getAllRules()) {
							if (mediaTopLevelRule instanceof CSSStyleRule) {
								appendCssStylePrefix((CSSStyleRule)mediaTopLevelRule, prefixMember, notMember, absoluteUrlPrefix, replaceHtmlBody);
							}
						}
					} else {
						// TODO: CSSImportRule
						logger.error("unknow style: " + cssTopLevelRule);
					}
					
					String text = cssTopLevelRule.getAsCSSString(cssSettings, 0);
					newContent.append(text).append('\n');
				}
				return newContent.toString();
			} catch (final Exception ex) {
				logger.error("Failed to write the CSS to a file", ex);
			}
			
			return newContent.toString();
		} catch (Exception ex) {
			logger.error("Failed to parse css", ex);
			return content;
		}
	}

	private void appendCssStylePrefix(CSSStyleRule cssStyleRule, CSSSelectorSimpleMember prefixMember,
			CSSSelectorMemberNot notMember, String absoluteUrlPrefix, boolean replaceHtmlBody) {
		for (CSSSelector cssSelector : cssStyleRule.getAllSelectors()) {
			String cssName = cssSelector.getMemberAtIndex(0).getAsCSSString(cssSettings, 0);
			if (!cssName.startsWith("._")) {			// don't change the private class
				int memberCount = cssSelector.getMemberCount();
				String lastCssName = cssSelector.getMemberAtIndex(memberCount - 1)
						.getAsCSSString(cssSettings, 0);
				
				// add prefix
				if (replaceHtmlBody) {
					if ("body".equalsIgnoreCase(lastCssName) || "html".equalsIgnoreCase(lastCssName)) {
						// replace the 'body' or 'html' to '.prefix'
						cssSelector.removeMember(memberCount - 1);
						cssSelector.addMember(memberCount - 1, prefixMember);
					} else {
						cssSelector.addMember(0, prefixMember);
						cssSelector.addMember(1, ECSSSelectorCombinator.BLANK);
					}
				} else {
					// don't add prefix to 'html' and 'body'
					if (!"body".equalsIgnoreCase(lastCssName) && !"html".equalsIgnoreCase(lastCssName)) {
						cssSelector.addMember(0, prefixMember);
						cssSelector.addMember(1, ECSSSelectorCombinator.BLANK);
					}
				}
				
				if (notMember != null) {
					// 1) don't append more than 2 suffix
					if (lastCssName.charAt(0) != ':') {
						cssSelector.addMember(notMember);
					}
				}
			}
		}
		replaceStyleUrlToAbsoluteUrl(cssStyleRule, absoluteUrlPrefix);
	}

	private void replaceStyleUrlToAbsoluteUrl(CSSStyleRule styleRule, String absoluteUrlPrefix) {
		CSSDeclaration bgImgCssValue = styleRule.getDeclarationOfPropertyName("background-image");
		if (bgImgCssValue != null) {
			replaceUrlToAbsoluteUrl(bgImgCssValue, absoluteUrlPrefix);
		}
		CSSDeclaration bgCssValue = styleRule.getDeclarationOfPropertyName("background");
		if (bgCssValue != null) {
			replaceUrlToAbsoluteUrl(bgCssValue, absoluteUrlPrefix);
		}
		// cursor
		CSSDeclaration cursorCssValue = styleRule.getDeclarationOfPropertyName("cursor");
		if (cursorCssValue != null) {
			replaceUrlToAbsoluteUrl(cursorCssValue, absoluteUrlPrefix);
		}
	}
	
	private boolean replaceUrlToAbsoluteUrl(CSSDeclaration cssValue, 
			String absoluteUrlPrefix) {
		CSSExpression expression = cssValue.getExpression();
		for (ICSSExpressionMember member : expression.getAllMembers()) {
			if (member instanceof CSSExpressionMemberTermURI) {
				CSSExpressionMemberTermURI uriMember = (CSSExpressionMemberTermURI)member;
				String url = uriMember.getURIString();
				if (url != null && url.length() > 0 && url.charAt(0) != '/') {
					String absoluteUrl = absoluteUrlPrefix + url;
					uriMember.setURIString(absoluteUrl);
					return true;
				} 
			}
		}
		return false;
	}
}